package main

import (
    "bytes"
    "crypto/des"
    "encoding/base64"
    "encoding/json"
    "flag"
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
    "strings"
    "time"
)

const desKey = "ourheyday-yuandaima"
const flagFile = "/etc/bindid.flag"

func getSN() string {
    content, err := ioutil.ReadFile("/proc/cmdline")
    if err == nil {
        fields := strings.Fields(string(content))
        for _, field := range fields {
            if strings.HasPrefix(field, "snum=") {
                sn := strings.TrimPrefix(field, "snum=")
                if sn != "" {
                    return sn
                }
            }
        }
    }
    
    machineId, err := ioutil.ReadFile("/etc/machine-id")
    if err != nil {
        return ""
    }
    return strings.TrimSpace(string(machineId))
}

func setFlag(sn string) error {
    return ioutil.WriteFile(flagFile, []byte(sn), 0600)
}

func alreadyFlag(sn string) bool {
    data, err := ioutil.ReadFile(flagFile)
    if err != nil {
        return false
    }
    return strings.TrimSpace(string(data)) == sn
}

func pkcs5Padding(src []byte, blockSize int) []byte {
    pad := blockSize - len(src)%blockSize
    padding := bytes.Repeat([]byte{byte(pad)}, pad)
    return append(src, padding...)
}

func pkcs5Unpadding(src []byte) []byte {
    l := len(src)
    if l == 0 {
        return src
    }
    pad := int(src[l-1])
    return src[:l-pad]
}

func desEncrypt(plain string) (string, error) {
    key := []byte(desKey)[:8]
    block, err := des.NewCipher(key)
    if err != nil {
        return "", err
    }
    data := pkcs5Padding([]byte(plain), block.BlockSize())
    encrypted := make([]byte, len(data))
    for bs, be := 0, block.BlockSize(); bs < len(data); bs, be = bs+block.BlockSize(), be+block.BlockSize() {
        block.Encrypt(encrypted[bs:be], data[bs:be])
    }
    return base64.StdEncoding.EncodeToString(encrypted), nil
}

func desDecrypt(cipherText string) (string, error) {
    key := []byte(desKey)[:8]
    block, err := des.NewCipher(key)
    if err != nil {
        return "", err
    }
    decoded, err := base64.StdEncoding.DecodeString(cipherText)
    if err != nil {
        return "", err
    }
    decrypted := make([]byte, len(decoded))
    for bs, be := 0, block.BlockSize(); bs < len(decoded); bs, be = bs+block.BlockSize(), be+block.BlockSize() {
        block.Decrypt(decrypted[bs:be], decoded[bs:be])
    }
    unpadded := pkcs5Unpadding(decrypted)
    return string(unpadded), nil
}

func printHelp() {
    fmt.Println(`Usage:
  bindid --api-url <url> --access-type <type>

Options:
  --api-url      Specify API endpoint URL. (Required)
  --access-type  Specify access type (Required): 
                 0 = android binary, 
                 1 = linux binary, 
                 2 = x86-ISO image, 
                 3 = docker container.
  --help         Show this help message.

This program automatically detects device SN, submits information to the provided API endpoint,
and saves a flag at '` + flagFile + `' after successful submission to avoid resubmission.`)
}

func main() {
    apiUrl := flag.String("api-url", "", "API endpoint URL to submit binding info")
    accessType := flag.Int("access-type", -1, "Access type: 0=android, 1=linux, 2=x86-ISO, 3=docker")
    showHelp := flag.Bool("help", false, "Show help information")
    flag.Usage = printHelp
    flag.Parse()

    if *showHelp {
        printHelp()
        return
    }
    if *apiUrl == "" || *accessType < 0 || *accessType > 3 {
        printHelp()
        os.Exit(1)
    }

    sn := getSN()
    if sn == "" {
        fmt.Println("Failed to obtain sn from /proc/cmdline or /etc/machine-id. Exiting.")
        os.Exit(1)
    }

    // 如果已有标志文件且内容一致，直接退出
    if alreadyFlag(sn) {
        fmt.Printf("Device already submitted. sn=%s. Skipping submission.\n", sn)
        return
    }

    // 没有标志文件或内容不一致，开始尝试上报
    for {
        reqObj := map[string]interface{}{
            "bindType":   "1",
            "sn":         sn,
            "accessType": *accessType,
        }
        plainObj, _ := json.Marshal(reqObj)
        encryptedObj, err := desEncrypt(string(plainObj))
        if err != nil {
            fmt.Println("Encryption error:", err)
            os.Exit(1)
        }

        reqBodyMap := map[string]interface{}{
            "result": true,
            "object": encryptedObj,
        }
        reqBody, _ := json.Marshal(reqBodyMap)

        req, err := http.NewRequest("POST", *apiUrl, bytes.NewReader(reqBody))
        if err != nil {
            fmt.Println("Error creating HTTP request:", err)
            os.Exit(1)
        }
        req.Header.Set("Content-Type", "application/json")

        client := &http.Client{}
        resp, err := client.Do(req)
        if err != nil {
            fmt.Println("HTTP request failed:", err)
            fmt.Println("Will retry in 5 minutes...")
            time.Sleep(5 * time.Minute)
            continue
        }
        respBytes, _ := ioutil.ReadAll(resp.Body)
        resp.Body.Close()

        type ApiResp struct {
            Result bool   `json:"result"`
            Code   int    `json:"code"`
            Msg    string `json:"msg"`
            Data   string `json:"data"`
            Object string `json:"object"`
        }
        var jr ApiResp
        if err := json.Unmarshal(respBytes, &jr); err != nil {
            fmt.Println("Unexpected API response:", err)
            fmt.Println("Response body:", string(respBytes))
            fmt.Println("Will retry in 5 minutes...")
            time.Sleep(5 * time.Minute)
            continue
        }

        if jr.Code == 200 {
            setFlag(sn)
            fmt.Println("Submitted successfully. Message:", jr.Msg)
            if jr.Data != "" {
                fmt.Println("Received 'data' (encrypted):", jr.Data)
                if dec, err := desDecrypt(jr.Data); err == nil {
                    fmt.Println("Received 'data' (decrypted):", dec)
                }
            }
            break // 成功后退出循环
        } else {
            fmt.Printf("Submission failed (code=%d): %s\n", jr.Code, jr.Msg)
            fmt.Println("Will retry in 5 minutes...")
            time.Sleep(5 * time.Minute)
        }
    }
}


